iT邦幫忙

2025 iThome 鐵人賽

DAY 10
2
Modern Web

Angular 進階實務 30天系列 第 10

Day 10:Router 基礎設計:單層與巢狀

  • 分享至 

  • xImage
  •  

前言

由於 Angular 是 單頁應用程式 (SPA) ,是透過 JavaScript 動態更新頁面內容,而不是重新載入整個頁面。

可以把 這個標籤想成一個框,這個框放進去誰,就顯示誰。

程式上,當URL改變的時候,Router 會:

  1. 解析當前的 URL 路徑
  2. 找到對應的路由配置
  3. 載入對應的 Component
  4. 將 Component 渲染到 <router-outlet>

實際運作流程:

使用者點擊連結 → URL 改變 → Router 偵測到變化 →
查找路由表 → 載入對應 Component → 替換框內內容


常見路由設計

當應用服務的對象單純,或是這是一個展示概念用的專案,通常單層的設計可以應付,依照需求跟設計,目前我使用過的有兩種。

另外為保持使用者體驗良好,都會加上Loading的元件,讓他們知道系統正在處理中,也可以減少工程師聽到使用者在問:為什麼都沒在動,同時,避免使用者重複操作相同行為,因為loading元件會全畫面覆蓋,防止重複操作。

單層

固定佈局設計

需要保持一致性跟導航的時候

🏠 index.html
└── 🛋️ app.component.html (有固定佈局)
    ├── ⏳ LoadingComponent (開燈中)
    ├── 🧭 導航列 (固定家具)
    ├── 🖼️ <router-outlet> (相框)
    │   └── 📷 Component (只是內容,沒有完整佈局)
    └── 📱 頁尾 (固定家具)

靈活設計

需要靈活設計,比如說登入、註冊、活動頁,整個版型會落差很大

🏠 index.html (劇院建築)
└── 🛋️ app.component.html (舞台)
    ├── ⏳ LoadingComponent (布幕/準備中的告示)
    └── 🖼️ <router-outlet> (表演區域)
        └── 📷 Component (不同的戲劇/表演)

巢狀

在比較大型的後台,通常是使用巢狀的設計,在 LayoutModule 裡面還會有多的 Lazy Loading,因為你需要服務多個單位,可能有產品A團隊、產品B團隊、產品A、B的管理者團隊,這時候就會切子路由 + Lazy Loading 來處理

🏠 index.html
└── 🎬 app.component.html (極簡)
    ├── ⏳ LoadingComponent 
    └── 🎭 <router-outlet> (主路由)
        ├── 🔐 LoginComponent (完整頁面)
        └── 📱 LayoutModule 
            ├── 🧭 導航列
            ├── 🖼️ <router-outlet> (子路由)
            │   ├── 📊 DashboardModule (lazy loading)
            │   └── 👥 UsersModule (lazy loading)
            └── 📱 頁尾

🔧 設計彈性提醒
不過因為Router-outlet的特性,都可以再根據需求去重新組裝單層跟我提供的嵌套的使用方式,可以把我提供的單層視為一個單元去組裝就可以了。


實際寫法

由於Loading跟Guard不是本章的重點,所以這裡不會放上程式碼,後續的章節才會放上

Step 1: 註冊路由(main.ts)

首先要先註冊路由

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),// 這裡註冊了 app.routes.ts 的路由配置
    // 其他 providers...
  ]
}).catch(err => console.error(err));

Step 2: 設定主要組件 (app.component.ts)

這裡單純只有使用 loading 跟 router-outlet,在import引入相關元件後就可以了

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LoadingComponent } from './core/loading/components/loading.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, LoadingComponent],
  template:`<app-loading></app-loading><router-outlet></router-outlet>`
})
export class AppComponent {}

Step 3: 設定主路由 (app.routes.ts)

之後在Routes這個檔案設定好,路由器會自動監聽瀏覽器的 URL 變化並更新視圖,由於 登入頁面沒有固定的頁首頁尾,所以跟 Layout 切開處理。

import { Routes } from '@angular/router';
import { LoginComponent } from './layouts/login/login.component';
import { APP_ROUTES } from './core/const/route.const';
import { loginGuard } from './core/guard/login.guard';

export const routes: Routes = [
  { path: APP_ROUTES.LOGIN, component: LoginComponent },
  {
    path: '', // 定義了子路由
    loadChildren: () => import('./layouts/layout.route').then(m => m.LAYOUT_ROUTES),
    canActivate: [loginGuard]
  }
];

Step 4: 設定佈局路由 (layout.routes.ts)

Angular 透過 app.routes.ts 中的 loadChildren 建立了這個連接關係。
而路由器透過 loadChildren 載入了 layout.routes.ts,然後發現其中有 MainLayoutComponent 和各個子路由,依此類推

import { Routes } from '@angular/router';
import { APP_ROUTES } from '../core/const/route.const';
import { MainLayoutComponent } from './main-layout/main-layout.component';

export const LAYOUT_ROUTES: Routes = [
  {
    path: '', component: MainLayoutComponent,
    children: [ // 定義了子路由
      { 
        path: APP_ROUTES.DOCUMENT_APPLICATION, 
        loadChildren: () => import('../features/document-application/document-application.routes').then(m => m.DOCUMENT_APPLICATION_ROUTES) 
      },
      { 
        path: APP_ROUTES.APPROVAL_CENTER, 
        loadChildren: () => import('../features/approval-center/approval-center.routes').then(m => m.APPROVAL_CENTER_ROUTES) 
      },
      { 
        path: APP_ROUTES.SYSTEM_MANGE, 
        loadChildren: () => import('../features/system-mange/system-mange.routes').then(m => m.SYSTEM_MANGE_ROUTES) 
      }
    ]
  }
];

路由連接的完整流程

我在一開始學的時候,主要還是抄前人跟網路上的寫法,常常處於知其然而不知其所以然的狀況,並不知道他們彼此之間怎麼溝通的。

畫成簡易的流程圖的話大概如下。

main.ts 
   │
   │ provideRouter(routes)
   ▼
app.routes.ts
   │
   │ bootstrapApplication(AppComponent)
   ▼
app.component.ts
   │
   │ <router-outlet> + loadChildren 觸發
   ▼
layout.routes.ts (layout.route.ts)
   │
   │ component: MainLayoutComponent
   ▼
main-layout.component.ts

1. Angular 路由器的工作機制

當 URL 匹配到 path: '' 時,Angular 會:

  1. 執行 loadChildren 函數
  2. 動態載入 layout.route.ts 模組
  3. 取得 LAYOUT_ROUTES 配置
  4. LAYOUT_ROUTES 當作子路由系統來處理

2. 實際執行過程

  1. 動態 import:載入 layout.route.ts 檔案
  2. 取得 LAYOUT_ROUTES:獲取路由配置陣列
  3. 合併路由樹:將 LAYOUT_ROUTES 合併到應用程式的路由樹中
  4. 建立組件關聯:Angular 看到 component: MainLayoutComponent,知道要渲染這個組件

3. 路由樹的合併過程

原本的路由樹:

app.routes.ts
├── /login → LoginComponent
└── / → (要載入子路由)

載入 LAYOUT_ROUTES 後變成:

合併後的路由樹
├── /login → LoginComponent
└── / → MainLayoutComponent
    ├── /document-application → ...
    ├── /approval-center → ...
    └── /system-mange → ...

Angular 路由載入流程示意圖

1. 應用程式啟動階段

┌─────────────────────────────────────────────────────────────┐
│                        main.ts                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ bootstrapApplication(AppComponent, {                │    │
│  │   providers: [                                      │    │
│  │     provideRouter(routes) ←── import from app.routes│    │
│  │   ]                                                 │    │
│  │ })                                                  │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Angular 路由器                           │
│             註冊 app.routes.ts 中的路由配置                  │
│                                                             │
│  routes = [                                                 │
│    { path: 'login', component: LoginComponent },            │
│    { path: '', loadChildren: () => import(...) }            │
│  ]                                                          │
└─────────────────────────────────────────────────────────────┘

2. URL 匹配與組件載入階段

用戶訪問 URL: '/' 或 '/document-application'
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    路由匹配過程                              │
│                                                             │
│  ┌─────────────────┐     ┌───────────────────────────────┐  │
│  │ URL: '/'        │ ──→ │ 匹配到 path: ''                │  │
│  │ 或其他子路徑     │     │ (在 app.routes.ts 中)         │  │
│  └─────────────────┘     └───────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                     執行 loadChildren                       │
│                                                             │
│  loadChildren: () => import('./layouts/layout.route')       │
│                     .then(m => m.LAYOUT_ROUTES)             │
│                              │                              │
│                              ▼                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │           動態載入 layout.route.ts                  │    │
│  │                                                     │    │
│  │  export const LAYOUT_ROUTES: Routes = [             │    │
│  │    {                                                │    │
│  │      path: '',                                      │    │
│  │      component: MainLayoutComponent, ←── 關鍵       │    │
│  │      children: [...]                                │    │
│  │    }                                                │    │
│  │  ]                                                  │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    組件渲染階段                              │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                AppComponent                         │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │            <router-outlet>                  │    │    │
│  │  │                    │                        │    │    │
│  │  │                    ▼                        │    │    │
│  │  │          MainLayoutComponent                │    │    │
│  │  │  ┌─────────────────────────────────────┐    │    │    │
│  │  │  │           側邊選單                  │    │    │    │
│  │  │  │           頂部導航                  │    │    │    │
│  │  │  │                                     │    │    │    │
│  │  │  │         <router-outlet>             │    │    │    │
│  │  │  │              │                      │    │    │    │
│  │  │  │              ▼                      │    │    │    │
│  │  │  │      (準備渲染子路由組件)            │    │    │    │
│  │  │  └─────────────────────────────────────┘    │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

3. 子路由匹配階段 (當 URL 為 '/document-application/create' 時)

┌─────────────────────────────────────────────────────────────┐
│                      URL 解析                               │
│                                                             │
│  URL: '/document-application/create'                        │
│          │                    │                             │
│          ▼                    ▼                             │
│      第一段:''           剩餘:'document-application/create'│
│    (匹配到 MainLayout)      (傳給子路由系統處理)             │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                 子路由匹配 (在 MainLayout 的 children 中)    │
│                                                             │
│  children: [                                                │
│    {                                                        │
│      path: 'document-application',                          │
│      loadChildren: () => import('...').then(...)            │
│    }                                                        │
│  ]                                                          │
│                              │                              │
│                              ▼                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │        載入 document-application.routes.ts          │    │
│  │                                                     │    │
│  │  繼續匹配 '/create' 路徑                             │    │
│  │  找到對應組件                                        │    │
│  │                                                     │    │
│  │  渲染在 MainLayoutComponent 的 <router-outlet> 中    │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

4. 最終渲染結果

┌─────────────────────────────────────────────────────────────┐
│                     瀏覽器畫面                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                AppComponent                         │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │          MainLayoutComponent                │    │    │
│  │  │ ┌─────────┐ ┌─────────────────────────────┐ │    │    │
│  │  │ │ 側邊選單 │ │        內容區域             │ │    │    │
│  │  │ │ (固定)  │ │                             │ │    │    │
│  │  │ │         │ │  DocumentCreateComponent    │ │    │    │
│  │  │ │ - 文件  │ │  (或其他子路由組件)          │ │    │    │
│  │  │ │ - 審核  │ │                             │ │    │    │
│  │  │ │ - 系統  │ │  ← 這裡會根據 URL 切換       │ │    │    │
│  │  │ │         │ │     但選單和導航保持不變     │ │    │    │
│  │  │ └─────────┘ └─────────────────────────────┘ │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

總結重點

  • main.ts 只負責註冊根路由 (app.routes.ts)
  • app.routes.ts 透過 loadChildren 建立與 layout.routes.ts 的連接
  • MainLayoutComponent 不需要知道路由配置,只需提供 <router-outlet>
  • Angular 路由器 負責整個載入和匹配過程
  • 兩層 <router-outlet> 實現了巢狀路由的渲染

上一篇
Day9:新增、修改跟刪除
下一篇
Day 11:Router - 參數傳遞
系列文
Angular 進階實務 30天21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言